﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Animation;
using System.Windows;
using SlimDX;
using System.ComponentModel;

namespace System.Windows.Media.Media3D10
{
    /// <summary>
    /// Représente une caméra de projection en perspective. 
    /// </summary>
    public sealed class Camera : Animatable
    {
        #region DependencyProperties
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Eye"/>.
        /// </summary>
        public static readonly DependencyProperty EyeProperty =
            DependencyProperty.Register("Eye", typeof(Vector3), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(Eye_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Target"/>.
        /// </summary>
        public static readonly DependencyProperty TargetProperty =
            DependencyProperty.Register("Target", typeof(Vector3), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(Target_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Width"/>.
        /// </summary>
        public static readonly DependencyProperty WidthProperty =
            DependencyProperty.Register("Width", typeof(double), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(Width_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Height"/>.
        /// </summary>
        public static readonly DependencyProperty HeightProperty =
            DependencyProperty.Register("Height", typeof(double), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(Height_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="NearPlaneDistance"/>.
        /// </summary>
        public static readonly DependencyProperty NearPlaneDistanceProperty =
            DependencyProperty.Register("NearPlaneDistance", typeof(float), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(NearPlaneDistance_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="FarPlaneDistance"/>.
        /// </summary>
        public static readonly DependencyProperty FarPlaneDistanceProperty =
            DependencyProperty.Register("FarPlaneDistance", typeof(float), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(FarPlaneDistance_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Transform"/>.
        /// </summary>
        public static readonly DependencyProperty TransformProperty =
            DependencyProperty.Register("Transform", typeof(Transform3D10), typeof(Camera),
            new PropertyMetadata(new PropertyChangedCallback(Transform_PropertyChanged)));
        #endregion

        #region DependencyProperties Callbacks
        private static void Eye_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnEyePropertyChanged(e);
        }

        private static void Target_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnTargetPropertyChanged(e);
        }

        private static void NearPlaneDistance_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnNearPlaneDistancePropertyChanged(e);
        }

        private static void FarPlaneDistance_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnFarPlaneDistancePropertyChanged(e);
        }

        private static void Width_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnWidthPropertyChanged(e);
        }

        private static void Height_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnHeightPropertyChanged(e);
        }

        private static void Transform_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ((Camera)sender).OnTransformPropertyChanged(e);
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Initialise une nouvelle instance de la classe <see cref="Camera"/>
        /// </summary>
        public Camera()
            : base()
        {
            this.Eye = Vector3.Zero;
            this.Target = Vector3.Zero;
            this.Transform = Transform3D10.Identity;
        }
        #endregion

        #region Data
        private SlimDX.Matrix _viewMatrix;
        private SlimDX.Matrix _projectionMatrix;

        private SlimDX.Vector3 _eyeWorldPosition;
        #endregion

        #region Public Properties
        /// <summary>
        /// Position de la camera
        /// </summary>
        [TypeConverter(typeof(Vector3TypeConverter))]
        public Vector3 Eye
        {
            get { return (Vector3)GetValue(EyeProperty); }
            set { SetValue(EyeProperty, value); }
        }

        /// <summary>
        /// Cible de la camera
        /// </summary>
        [TypeConverter(typeof(Vector3TypeConverter))]
        public Vector3 Target
        {
            get { return (Vector3)GetValue(TargetProperty); }
            set { SetValue(TargetProperty, value); }
        }

        /// <summary>
        /// Obtient ou définit une valeur qui spécifie la distance à partir de la caméra du plan de clip proche. Il s'agit d'une propriété de dépendance. 
        /// </summary>
        public float NearPlaneDistance
        {
            get { return (float)GetValue(NearPlaneDistanceProperty); }
            set { SetValue(NearPlaneDistanceProperty, value); }
        }

        /// <summary>
        /// Obtient ou définit une valeur qui spécifie la distance à partir de la caméra du plan de clip lointain. Il s'agit d'une propriété de dépendance. 
        /// </summary>
        public float FarPlaneDistance
        {
            get { return (float)GetValue(FarPlaneDistanceProperty); }
            set { SetValue(FarPlaneDistanceProperty, value); }
        }

        /// <summary>
        /// Largeur de la surface de projection
        /// </summary>
        public double Width
        {
            get { return (double)GetValue(WidthProperty); }
            set { SetValue(WidthProperty, value); }
        }

        /// <summary>
        /// Hauteur de la surface de projection
        /// </summary>
        public double Height
        {
            get { return (double)GetValue(HeightProperty); }
            set { SetValue(HeightProperty, value); }
        }

        /// <summary>
        /// Obtient ou définit la <see cref="Transform"/> appliquée à la caméra. Il s'agit d'une propriété de dépendance.
        /// </summary>
        public Transform3D10 Transform
        {
            get { return (Transform3D10)GetValue(TransformProperty); }
            set { SetValue(TransformProperty, value); }
        }

        /// <summary>
        /// Obtient la position de la camera après application de sa transromation.
        /// </summary>
        public SlimDX.Vector3 EyeWorldPosition
        {
            get { return _eyeWorldPosition; }
        }
        #endregion

        #region Internal Members
        internal SlimDX.Matrix ViewMatrix
        {
            get { ComputeViewMatrix(); return _viewMatrix; }
        }

        internal SlimDX.Matrix ProjectionMatrix
        {
            get { return _projectionMatrix; }
        }
        #endregion

        #region Private Members
        private void OnEyePropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeViewMatrix();
        }

        private void OnTargetPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeViewMatrix();
        }

        private void OnNearPlaneDistancePropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeProjectionMatrix();
        }

        private void OnFarPlaneDistancePropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeProjectionMatrix();
        }

        private void OnWidthPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeProjectionMatrix();
        }

        private void OnHeightPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeProjectionMatrix();
        }

        private void OnTransformPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            ComputeViewMatrix();
        }

        private void ComputeProjectionMatrix()
        {
            float ratio = (float)(this.Width / this.Height);
            _projectionMatrix = SlimDX.Matrix.PerspectiveFovRH(0.7f, ratio, this.NearPlaneDistance, this.FarPlaneDistance);
        }

        private void ComputeViewMatrix()
        {
            Vector3 up = new Vector3(0, 0, 1.0f);
            Vector3 radius = this.Eye - this.Target;
            Vector3 eye = this.Target + Transform.Transform(radius);
            SlimDX.Matrix rawViewMatrix = SlimDX.Matrix.LookAtRH(eye, this.Target, up);
            _viewMatrix = rawViewMatrix;
            ComputeWorldPosition();
        }

        private void ComputeWorldPosition()
        {
            SlimDX.Matrix viewMatrixInverse = SlimDX.Matrix.Invert(_viewMatrix);
            _eyeWorldPosition = Vector3.TransformCoordinate(Vector3.Zero, viewMatrixInverse);
        }
        #endregion

        #region Freezable Implementation
        /// <summary>
        /// Une fois implémentée dans une classe dérivée, initialise une nouvelle instance de la classe dérivée <see cref="Freezable"/>. 
        /// </summary>
        /// <returns>Nouvelle instance.</returns>
        protected override Freezable CreateInstanceCore()
        {
            return new Camera();
        } 
        #endregion
    }
}
